package com.hrbxlh.virtualization.service.impl;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.NetUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.core.util.ZipUtil;
import com.hrbxlh.virtualization.constant.VolumeConstant;
import com.hrbxlh.virtualization.dao.*;
import com.hrbxlh.virtualization.dto.FlavorDTO;
import com.hrbxlh.virtualization.enums.ContainerRoleEnum;
import com.hrbxlh.virtualization.enums.RoleEnum;
import com.hrbxlh.virtualization.exception.GlobalException;
import com.hrbxlh.virtualization.param.ClipboardParam;
import com.hrbxlh.virtualization.param.DesktopShareParam;
import com.hrbxlh.virtualization.pojo.*;
import com.hrbxlh.virtualization.result.ResultEnum;
import com.hrbxlh.virtualization.service.ClusterService;
import com.hrbxlh.virtualization.service.ContainerService;
import com.hrbxlh.virtualization.service.DesktopService;
import com.hrbxlh.virtualization.service.DockerService;
import com.hrbxlh.virtualization.util.LockUtil;
import com.hrbxlh.virtualization.util.SessionUtil;
import com.hrbxlh.virtualization.vo.DesktopVO;
import com.hrbxlh.virtualization.vo.UserListVO;
import com.hrbxlh.virtualization.vo.UserVO;
import com.hrbxlh.virtualization.websocket.DesktopWebSocket;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @author cheer
 */
@Service
public class DesktopServiceImpl implements DesktopService {

    private Logger logger = LoggerFactory.getLogger(DesktopServiceImpl.class);

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private ImageAllocationMapper imageAllocationMapper;

    @Autowired
    private ImageMapper imageMapper;

    @Autowired
    private ClusterMapper clusterMapper;

    @Autowired
    private ContainerMapper containerMapper;

    @Autowired
    private ContainerService containerService;

    @Autowired
    private DesktopMapper desktopMapper;

    @Autowired
    private DockerService dockerService;

    @Autowired
    private StatisticMapperExt statisticMapperExt;

    @Autowired
    private ClusterService clusterService;

    @Override
    public DesktopVO queryDesktop(String stuorteaId, Integer courseId, Integer chapterId) {
        ImageAllocation imageAllocation = getImageAllocation(courseId, chapterId);
        if (imageAllocation == null) {
            throw new GlobalException(ResultEnum.IMAGE_ALLOCATION_NOT_EXISTS);
        }

        if (imageAllocation.getCluster()) {
            return getDesktopByCluster(stuorteaId, courseId, chapterId, imageAllocation.getClusterId(),
                    imageAllocation.getFlavor());
        } else {
            return getDesktopByImage(stuorteaId, courseId, chapterId, imageAllocation.getImageId(),
                    imageAllocation.getFlavor());
        }
    }

    @Override
    public DesktopVO replaceDesktop(String stuorteaId, Integer courseId, Integer chapterId) {
        deleteContainer(stuorteaId, courseId, chapterId);
        return queryDesktop(stuorteaId, courseId, chapterId);
    }

    @Override
    public void downloadFile(String stuorteaId, Integer courseId, Integer chapterId) {
        String filePath = getVolumePath(stuorteaId, courseId, chapterId);
        if (!FileUtil.exist(filePath)) {
            return;
        }

        String zipPath = getZipPath(stuorteaId, courseId, chapterId);
        ZipUtil.zip(filePath, zipPath);

        HttpServletResponse response =
                ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        try (InputStream inputStream = new FileInputStream(zipPath);
             OutputStream outputStream = response.getOutputStream()) {

            response.setContentType("application/x-download");
            String downloadName = generateFileName(stuorteaId, courseId, chapterId);
            response.addHeader("Content-Disposition",
                    "attachment;filename=" + downloadName + ";filename*=UTF-8''" + URLUtil.encode(downloadName));
            IOUtils.copy(inputStream, outputStream);
            outputStream.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void clipboard(ClipboardParam clipboardParam) {
        Container container = containerMapper.selectByPrimaryKey(clipboardParam.getContainerId());
        if (container == null) {
            throw new GlobalException(ResultEnum.CONTAINER_NOT_EXISTS);
        }

        String host = dockerService.queryHost(container.getRancherId());
        String url = String.format("http://%s:56000/clipboard", host);
        HttpEntity httpEntity = generateHttpEntity(NetUtil.longToIpv4(container.getIp()), clipboardParam.getContent());
        try {
            String result = restTemplate.postForObject(url, httpEntity, String.class);
            if(Objects.equals(result, "fail")){
                throw new GlobalException(ResultEnum.CLIPBOARD_ERROR);
            }
        } catch (Exception e) {
            logger.error("发送剪切板请求失败：容器RancherId - {}", container.getRancherId(), e);
            throw new GlobalException(ResultEnum.CLIPBOARD_ERROR);
        }
    }

    @Override
    public void upload(Long containerId,MultipartFile file)throws IOException  {
        Container container = containerMapper.selectByPrimaryKey(containerId);
        if (container == null) {
            throw new GlobalException(ResultEnum.CONTAINER_NOT_EXISTS);
        }

        String filePath = "./tmp/container";
        FileUtil.mkdir(filePath);
        filePath = filePath + "/" + file.getOriginalFilename();
        try {
            file.transferTo(new File(filePath));
            HttpHeaders headers = new HttpHeaders();
            MediaType type = MediaType.parseMediaType("multipart/form-data;charset=UTF-8");
            headers.setContentType(type);

            MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
            File filenew = new File(filePath);
            String host = dockerService.queryHost(container.getRancherId());
            body.add("file", new FileSystemResource(filenew));
            body.add("ip",integer2Ip(container.getIp()));
            body.add("fileName",file.getOriginalFilename() );
            HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
            String path = String.format("http://%s:56000/upload",host);
            restTemplate.postForEntity(path, requestEntity,  Void.class);
            FileUtil.del(filePath);
        } catch (Exception e) {
            logger.error("文件上传请求失败：容器RancherId - {}", container.getRancherId(), e);
            throw new GlobalException(ResultEnum.UPLOAD_ERROR);
        }

    }

    @Override
    public UserListVO listOnlineUsers(Integer courseId, Integer chapterId, String stuorteaId) {
        String teacherId;
        if (Objects.equals(SessionUtil.getRoleId(), RoleEnum.STUDENT.getCode())) {
            teacherId = statisticMapperExt.getTeacherIdByStuorteaIdAndCourseId(stuorteaId, courseId);
        } else {
            teacherId = stuorteaId;
        }

        Set<UserVO> userVOSet = DesktopWebSocket.getUserVOByChapterId(teacherId, courseId, chapterId);

        UserListVO userListVO = new UserListVO();
        for (UserVO userVO : userVOSet) {
            if (Objects.equals(stuorteaId, userVO.getStuorteaId())) {
                continue;
            }
            if (Objects.equals(userVO.getRoleId(), RoleEnum.TEACHER.getCode())) {
                userListVO.getTeacherList().add(userVO);
            } else if (Objects.equals(userVO.getRoleId(), RoleEnum.STUDENT.getCode())) {
                userListVO.getStudentList().add(userVO);
            }
        }
        return userListVO;
    }

    //转化ip
    public static String integer2Ip(int ip) {
        StringBuilder sb = new StringBuilder();
        int num = 0;
        boolean needPoint = false; // 是否需要加入'.'
        for (int i = 0; i < 4; i++) {
            if (needPoint) {
                sb.append('.');
            }
            needPoint = true;
            int offset = 8 * (3 - i);
            num = (ip >> offset) & 0xff;
            sb.append(num);
        }
        return sb.toString();
    }
    @Override
    public void shareContainer(DesktopShareParam desktopShareParam, String stuorteaId) {
        Container container = containerMapper.selectByPrimaryKey(desktopShareParam.getContainerId());
        if (container == null) {
            throw new GlobalException(ResultEnum.CONTAINER_NOT_EXISTS);
        }
        if (!Objects.equals(container.getStuorteaId(), stuorteaId)) {
            throw new GlobalException(ResultEnum.DESKTOP_SHARE_NOT_ALLOWED);
        }
        DesktopWebSocket.shareContainer(container, desktopShareParam.getStuorteaIdList(),
                desktopShareParam.isControllable());
    }

    @Override
    public void unshareContainer(Long containerId, String stuorteaId) {
        Container container = containerMapper.selectByPrimaryKey(containerId);
        if (container == null) {
            throw new GlobalException(ResultEnum.CONTAINER_NOT_EXISTS);
        }
        if (!Objects.equals(container.getStuorteaId(), stuorteaId)) {
            throw new GlobalException(ResultEnum.DESKTOP_UNSHARE_NOT_ALLOWED);
        }
        DesktopWebSocket.stopShareContainer(container);
    }

    @Override
    public String ftp(Long containerId) {
        Container container = containerMapper.selectByPrimaryKey(containerId);
        if (container == null) {
            throw new GlobalException(ResultEnum.CONTAINER_NOT_EXISTS);
        }

        return dockerService.queryFtpUrl(container.getRancherId());
    }

    @Override
    public boolean isFileExists(String stuorteaId, Integer courseId, Integer chapterId) {
        String filePath = getVolumePath(stuorteaId, courseId, chapterId);
        return FileUtil.exist(filePath);
    }

    private ImageAllocation getImageAllocation(Integer courseId, Integer chapterId) {
        if (courseId == null || chapterId == null) {
            return null;
        }

        ImageAllocationExample imageAllocationExample = new ImageAllocationExample();
        imageAllocationExample.or().andCourseIdEqualTo(courseId)
                .andChapterIdEqualTo(chapterId);
        List<ImageAllocation> imageAllocationList = imageAllocationMapper.selectByExample(imageAllocationExample);

        if (CollectionUtils.isNotEmpty(imageAllocationList)) {
            return imageAllocationList.get(0);
        }
        return null;
    }

    private DesktopVO getDesktopByCluster(String stuorteaId, Integer courseId, Integer chapterId, Long clusterId,
                                          String flavor) {

        Cluster cluster = clusterMapper.selectByPrimaryKey(clusterId);
        if (cluster == null) {
            throw new GlobalException(ResultEnum.CLUSTER_ALLOCATION_DELETED);
        }
        Image masterImage = imageMapper.selectByPrimaryKey(cluster.getMasterImageId());
        Image firstSlaveImage = imageMapper.selectByPrimaryKey(cluster.getFirstSlaveImageId());
        Image secondSlaveImage = imageMapper.selectByPrimaryKey(cluster.getSecondSlaveImageId());
        if (masterImage == null || firstSlaveImage == null || secondSlaveImage == null) {
            throw new GlobalException(ResultEnum.CLUSTER_ALLOCATION_BROKEN);
        }

        DesktopVO desktopVO = new DesktopVO();
        desktopVO.setCluster(true);
        Container master = fillContainerToDesktopVO(stuorteaId, courseId, chapterId, masterImage, flavor,
                ContainerRoleEnum.MASTER, desktopVO);
        Container slave1 = fillContainerToDesktopVO(stuorteaId, courseId, chapterId, firstSlaveImage, flavor,
                ContainerRoleEnum.FIRST_SLAVE, desktopVO);
        Container slave2 = fillContainerToDesktopVO(stuorteaId, courseId, chapterId, secondSlaveImage, flavor,
                ContainerRoleEnum.SECOND_SLAVE, desktopVO);

        clusterService.modifyEtcHosts(master, slave1, slave2);
        return desktopVO;
    }

    private DesktopVO getDesktopByImage(String stuorteaId, Integer courseId, Integer chapterId, Long imageId,
                                        String flavor) {

        Image image = imageMapper.selectByPrimaryKey(imageId);
        if (image == null) {
            throw new GlobalException(ResultEnum.IMAGE_ALLOCATION_DELETED);
        }

        DesktopVO desktopVO = new DesktopVO();
        desktopVO.setCluster(false);
        fillContainerToDesktopVO(stuorteaId, courseId, chapterId, image, flavor, ContainerRoleEnum.SINGLE, desktopVO);
        return desktopVO;
    }

    private Container fillContainerToDesktopVO(String stuorteaId, Integer courseId, Integer chapterId, Image image,
                                               String flavor, ContainerRoleEnum containerRoleEnum, DesktopVO desktopVO) {

        // 加锁，防止用户同时打开多个实验环境页面的情况下创建出多个容器
        String lockKey = String.format("%s-%s-%s", stuorteaId, courseId, chapterId);
        try {
            if (!LockUtil.tryLock(lockKey, 10, TimeUnit.SECONDS)) {
                throw new GlobalException(ResultEnum.DESKTOP_BUSY);
            }

            Container container = getContainerByImage(stuorteaId, courseId, chapterId, image.getId(), containerRoleEnum);
            if (container != null) {
                if (!containerService.startContainer(container)) {
                    containerService.deleteContainer(container);
                    container = null;
                }
            }

            if (container == null) {
                FlavorDTO flavorDTO = parseFlavor(flavor, containerRoleEnum);
                String volumeName = generateVolumeName(stuorteaId, courseId, chapterId);
                String hostname = null;
                if (containerRoleEnum == ContainerRoleEnum.FIRST_SLAVE) {
                    hostname = "slave1";
                }
                if (containerRoleEnum == ContainerRoleEnum.SECOND_SLAVE) {
                    hostname = "slave2";
                }
                container = containerService.createContainer(image, volumeName, flavorDTO, hostname);
                insertContainer(container, stuorteaId, courseId, chapterId, containerRoleEnum);
            }

            String desktopUrl = dockerService.queryDesktopUrl(container.getRancherId());
            fillContainerToDesktopVO(container, desktopUrl, containerRoleEnum, desktopVO);
            return container;
        } finally {
            // 解锁
            LockUtil.unlock(lockKey);
        }
    }

    private void fillContainerToDesktopVO(Container container, String desktopUrl, ContainerRoleEnum containerRoleEnum,
                                          DesktopVO desktopVO) {

        switch (containerRoleEnum) {
            case SINGLE:
                desktopVO.setContainerId(container.getId());
                desktopVO.setImageId(container.getImageId());
                desktopVO.setDesktopUrl(desktopUrl);
                break;
            case MASTER:
                desktopVO.setMasterContainerId(container.getId());
                desktopVO.setMasterImageId(container.getImageId());
                desktopVO.setMasterDesktopUrl(desktopUrl);
                break;
            case FIRST_SLAVE:
                desktopVO.setFirstSlaveContainerId(container.getId());
                desktopVO.setFirstSlaveImageId(container.getImageId());
                desktopVO.setFirstSlaveDesktopUrl(desktopUrl);
                break;
            case SECOND_SLAVE:
                desktopVO.setSecondSlaveContainerId(container.getId());
                desktopVO.setSecondSlaveImageId(container.getImageId());
                desktopVO.setSecondSlaveDesktopUrl(desktopUrl);
                break;
            default:
        }
    }

    private Container getContainerByImage(String stuorteaId, Integer courseId, Integer chapterId, Long imageId,
                                          ContainerRoleEnum containerRoleEnum) {

        ContainerExample containerExample = new ContainerExample();
        containerExample.or().andStuorteaIdEqualTo(stuorteaId)
                .andCourseIdEqualTo(courseId)
                .andChapterIdEqualTo(chapterId)
                .andContainerRoleEqualTo(containerRoleEnum.getCode())
                .andImageIdEqualTo(imageId);
        List<Container> containerList = containerMapper.selectByExample(containerExample);
        if (CollectionUtils.isNotEmpty(containerList)) {
            Container container = containerList.get(0);
            if (dockerService.isContainerExists(container.getRancherId())) {
                return container;
            } else {
                containerService.deleteContainer(container);
            }
        }

        return null;
    }

    private FlavorDTO parseFlavor(String flavor, ContainerRoleEnum containerRoleEnum) {
        String[] flavorArray = StringUtils.split(flavor, ",");
        switch (containerRoleEnum) {
            case SINGLE:
            case MASTER:
                return new FlavorDTO(Integer.valueOf(flavorArray[0]), Integer.valueOf(flavorArray[1]));
            case FIRST_SLAVE:
                return new FlavorDTO(Integer.valueOf(flavorArray[2]), Integer.valueOf(flavorArray[3]));
            case SECOND_SLAVE:
                return new FlavorDTO(Integer.valueOf(flavorArray[4]), Integer.valueOf(flavorArray[5]));
            default:
                return null;
        }
    }

    private void insertContainer(Container container, String stuorteaId, Integer courseId, Integer chapterId,
                                 ContainerRoleEnum containerRoleEnum) {

        container.setStuorteaId(stuorteaId);
        container.setCourseId(courseId);
        container.setChapterId(chapterId);
        container.setContainerRole(containerRoleEnum.getCode());
        containerMapper.insertSelective(container);
    }

    private String generateVolumeName(String stuorteaId, Integer courseId, Integer chapterId) {
        return String.format("%s-%s-%s", stuorteaId, courseId, chapterId);
    }

    private void deleteContainer(String stuorteaId, Integer courseId, Integer chapterId) {
        ContainerExample containerExample = new ContainerExample();
        containerExample.or().andStuorteaIdEqualTo(stuorteaId)
                .andCourseIdEqualTo(courseId)
                .andChapterIdEqualTo(chapterId);
        List<Container> containerList = containerMapper.selectByExample(containerExample);
        for (Container container : containerList) {
            containerService.deleteContainer(container);
        }
    }

    private String getVolumePath(String stuorteaId, Integer courseId, Integer chapterId) {
        return String.format(VolumeConstant.NFS_DIR + "/%s-%s-%s/", stuorteaId, courseId, chapterId);
    }

    private String getZipPath(String stuorteaId, Integer courseId, Integer chapterId) {
        return String.format(VolumeConstant.NFS_DIR + "/%s-%s-%s.zip", stuorteaId, courseId, chapterId);
    }

    private String generateFileName(String stuorteaId, Integer courseId, Integer chapterId) {
        String username = desktopMapper.getUsernameByStuorteaId(stuorteaId);
        String courseName = desktopMapper.getCourseNameByCourseId(courseId);
        String chapterName = desktopMapper.getChapterNameByChapterId(chapterId);
        return String.format("%s-%s-%s-代码.zip", username, courseName, chapterName);
    }

    private HttpEntity generateHttpEntity(String ip, String content) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
        map.add("ip", ip);
        map.add("content", content);
        return new HttpEntity<>(map, headers);
    }
}
